package uk.ac.cam.bt288.DesktopApp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Map;

import javax.bluetooth.RemoteDevice;
import javax.microedition.io.StreamConnection;
import javax.swing.JLabel;

import com.intel.bluetooth.RemoteDeviceHelper;

import biz.source_code.base64Coder.Base64Coder;

public class BluetoothProcessConnection implements Runnable{

	private StreamConnection mConnection;
	private JLabel status = null;
	private KeyPair k = null;
	private String user = null;

	// Constant that indicate command from devices
	private static final int EXIT_CMD = 255;
	private static final int AUTHENTICATE = 1;
	private static final int NEWACCOUNT = 2;

	public BluetoothProcessConnection(StreamConnection connection, JLabel s)
	{
		mConnection = connection;
		status = s;
		try {
			k = Keys.readKeyPair("App");
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@Override
	public void run() {
		try {
			RemoteDevice dev = RemoteDevice.getRemoteDevice(mConnection);

			InputStream inputStream = mConnection.openInputStream();
			OutputStream outputStream = mConnection.openOutputStream();
			System.out.println("waiting for input");
			status.setText("Waiting for input");

			while (true) {
				int command = inputStream.read();

				if (command == EXIT_CMD)
				{
					System.out.println("finish process");
					status.setText("Finished process with current Pico");
					mConnection.close();
					break;
				}
				else if (command == AUTHENTICATE) {
					System.out.println(new Timestamp(System.currentTimeMillis()));
					if (authenticate(inputStream, outputStream) ) {
						System.out.println(new Timestamp(System.currentTimeMillis()));
						LoginInterface loggedIn = new LoginInterface();
						loggedIn.setVisible(true);
						int lastSignal = 0, currSignal = 0;
						while (true) {
							loggedIn.setStatus("Welcome "+user);
							loggedIn.unlock();
						
							lastSignal = currSignal;
							try {
								currSignal = RemoteDeviceHelper.readRSSI(dev);
							}
							catch (Exception e) {
								//Connection has lost
								break;
							}
							if (lastSignal<0 && currSignal<0 && currSignal<lastSignal) {
								int timer = 30;
								while (timer-->0) {
									loggedIn.lock();
									loggedIn.setStatus("Locked. Will log out in "+timer+" seconds");
									
									try {
										currSignal = RemoteDeviceHelper.readRSSI(dev);
										if (currSignal == 0)
											break;
									}
									catch (Exception e) {
										continue;
									}
									Thread.sleep(1000);
								}
								if (timer > 0)
									continue;
								else
									break;
							}
							Thread.sleep(20);
						}
						mConnection.close();
						loggedIn.dispose();
						break;
					}
				}
				else if (command == NEWACCOUNT) {
					newAccount(inputStream, outputStream);
					status.setText("Waiting for input");
				}
				else {
					System.out.println("Invalid command! " + command);
					break;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void newAccount (InputStream in, OutputStream out) {
		status.setText("Creating new account, please wait");
		Map<Key, String> m = AccountManagement.getAllAccounts();
		String username = null;

		int USER_EXISTS = 1;
		int OKAY = 0;

		byte[] clientKey = new byte[1024];
		byte[] message = new byte[128];

		int clientKeyBytes = -1;
		int messageLength = -1;

		//receive Pico's public key
		try {
			clientKeyBytes = in.read(clientKey);
			messageLength = in.read(message);

			String msg = Keys.decrypt(message, k.getPrivate());

			KeyFactory rsaKeyFac = KeyFactory.getInstance("RSA");
			PublicKey pubKey = rsaKeyFac.generatePublic(new X509EncodedKeySpec(Arrays.copyOfRange(clientKey, 0, clientKeyBytes)));
			out.write(Keys.encrypt(msg, pubKey));
			out.flush();

			messageLength = in.read(message);
			username = new String(Arrays.copyOfRange(message, 0, messageLength));

			System.err.println("Username: "+username);
			if (m.containsValue(username)) {
				out.write(USER_EXISTS);
				System.err.println("User: "+username+" exists");
			}
			else {
				AccountManagement.add(username, pubKey);
				out.write(OKAY);
				status.setText("User: "+username +" has been added!");
				Thread.sleep(4000);
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private boolean authenticate (InputStream in, OutputStream out) {
		status.setText("Authenticating, please wait");
		Map<Key, String> m = AccountManagement.getAllAccounts();
		String username = null;

		byte[] clientKey = new byte[1024];
		byte[] randMessage = new byte[128];

		int clientKeyBytes = -1;
		int messageLength = -1;

		//receive temporary public key from client, and a random message encrypted with app's public key
		try {
			clientKeyBytes = in.read(clientKey);
			messageLength = in.read(randMessage);
		} catch (IOException e) {
			e.printStackTrace();
		}
		String msg = null;

		System.err.println("clientKeyBytes: "+clientKeyBytes+"  messageLength: "+messageLength);
		System.err.println("Msg: "+new String(Base64Coder.encode(randMessage)));

		try {
			msg = Keys.decrypt(randMessage, k.getPrivate());
			//msg = Keys.decrypt(Keys.encrypt("abcd", k.getPublic()), k.getPrivate());
			System.err.println("Rand str: " + msg);
		} catch (Exception e) {
			e.printStackTrace();

		}

		//send decrypted message back to Pico, encoded with Pico's temporary public key to authenticate app
		KeyFactory rsaKeyFac;
		PublicKey pubKey = null;

		try {
			rsaKeyFac = KeyFactory.getInstance("RSA");
			pubKey = rsaKeyFac.generatePublic(new X509EncodedKeySpec(Arrays.copyOfRange(clientKey, 0, clientKeyBytes)));
			out.write(Keys.encrypt(msg, pubKey));
			out.flush();
		} catch (Exception e1) {
			e1.printStackTrace();
		}

		//receive client's real public key
		try {
			clientKeyBytes = in.read(clientKey);
		} catch (Exception e) {
			e.printStackTrace();
		}

		//generate random string to authenticate client

		try {
			rsaKeyFac = KeyFactory.getInstance("RSA");
			pubKey = rsaKeyFac.generatePublic(new X509EncodedKeySpec(Arrays.copyOfRange(clientKey, 0, clientKeyBytes)));

			if (!m.containsKey(pubKey)) {
				System.out.println("User has not registered");
				return false;
			}
			else {
				username = m.get(pubKey);
			}

			String randStr = Keys.nextSessionId();
			out.write(Keys.encrypt(randStr, pubKey));
			out.flush();
			System.err.println("Sent str: "+randStr);
			messageLength = in.read(randMessage);

			String clientAuth = Keys.decrypt(Arrays.copyOfRange(randMessage, 0, messageLength), k.getPrivate());

			System.err.println("Received msg: "+clientAuth);

			clientAuth = clientAuth.trim();
			randStr = randStr.trim();

			if (clientAuth.equals(randStr)) {
				System.out.println("Successfully authenticated "+username);
				user = username;
				//status.setText("Signed in as " + username);
				return true;
			}
			else {
				System.out.println("Authentication has failed");
				return false;
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;

	}
}
